VERSION 5.00
Begin VB.Form FrmEditor 
   AutoRedraw      =   -1  'True
   BackColor       =   &H00400040&
   BorderStyle     =   1  'Fixed Single
   Caption         =   " Pac Mar Level Editor"
   ClientHeight    =   7680
   ClientLeft      =   150
   ClientTop       =   255
   ClientWidth     =   9840
   Icon            =   "FrmEditor.frx":0000
   LinkTopic       =   "Form1"
   MaxButton       =   0   'False
   MinButton       =   0   'False
   ScaleHeight     =   512
   ScaleMode       =   3  'Pixel
   ScaleWidth      =   656
   StartUpPosition =   2  'CenterScreen
   Begin VB.PictureBox picHand 
      Appearance      =   0  'Flat
      AutoRedraw      =   -1  'True
      AutoSize        =   -1  'True
      BackColor       =   &H80000005&
      BorderStyle     =   0  'None
      ForeColor       =   &H80000008&
      Height          =   495
      Left            =   120
      ScaleHeight     =   33
      ScaleMode       =   3  'Pixel
      ScaleWidth      =   33
      TabIndex        =   11
      Top             =   600
      Visible         =   0   'False
      Width           =   495
   End
   Begin VB.ComboBox cmbDifficulty 
      BackColor       =   &H00400F27&
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   6.75
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   300
      ItemData        =   "FrmEditor.frx":000C
      Left            =   2160
      List            =   "FrmEditor.frx":001F
      Style           =   2  'Dropdown List
      TabIndex        =   9
      Top             =   7290
      Width           =   975
   End
   Begin VB.ComboBox cmbTheme 
      BackColor       =   &H00400F27&
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   6.75
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   300
      ItemData        =   "FrmEditor.frx":0043
      Left            =   480
      List            =   "FrmEditor.frx":0045
      Sorted          =   -1  'True
      Style           =   2  'Dropdown List
      TabIndex        =   8
      Top             =   7290
      Width           =   1095
   End
   Begin VB.VScrollBar VScrLevel 
      Enabled         =   0   'False
      Height          =   6720
      Left            =   9600
      TabIndex        =   1
      Top             =   480
      Width           =   255
   End
   Begin VB.PictureBox PicWorkspace 
      Appearance      =   0  'Flat
      AutoRedraw      =   -1  'True
      BackColor       =   &H8000000C&
      BorderStyle     =   0  'None
      DrawWidth       =   2
      ForeColor       =   &H80000008&
      Height          =   6720
      Left            =   0
      ScaleHeight     =   448
      ScaleMode       =   3  'Pixel
      ScaleWidth      =   640
      TabIndex        =   0
      Top             =   480
      Width           =   9600
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   0
      Left            =   0
      Tag             =   "New"
      Top             =   0
      Width           =   885
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   5
      Left            =   4800
      Tag             =   "Play"
      Top             =   0
      Width           =   945
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   4
      Left            =   3720
      Tag             =   "Revert"
      Top             =   0
      Width           =   945
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   3
      Left            =   2760
      Tag             =   "Undo"
      Top             =   0
      Width           =   945
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   2
      Left            =   1740
      Tag             =   "Save"
      Top             =   0
      Width           =   960
   End
   Begin VB.Image imgButton 
      Height          =   495
      Index           =   1
      Left            =   870
      Tag             =   "Load"
      Top             =   0
      Width           =   885
   End
   Begin VB.Label lblCookies 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "x 000"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Left            =   9240
      TabIndex        =   10
      Top             =   7320
      Width           =   405
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00240410&
      Index           =   7
      X1              =   216
      X2              =   216
      Y1              =   480
      Y2              =   512
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00A86684&
      Index           =   6
      X1              =   217
      X2              =   217
      Y1              =   481
      Y2              =   512
   End
   Begin VB.Image imgDifficulty 
      Height          =   240
      Left            =   1800
      Top             =   7320
      Width           =   240
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00A86684&
      Index           =   5
      X1              =   114
      X2              =   114
      Y1              =   481
      Y2              =   512
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00240410&
      Index           =   4
      X1              =   113
      X2              =   113
      Y1              =   480
      Y2              =   512
   End
   Begin VB.Image imgTheme 
      Height          =   240
      Left            =   120
      Top             =   7320
      Width           =   240
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00A86684&
      Index           =   3
      X1              =   317
      X2              =   317
      Y1              =   0
      Y2              =   31
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00240410&
      Index           =   2
      X1              =   318
      X2              =   318
      Y1              =   1
      Y2              =   33
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00240410&
      Index           =   1
      X1              =   181
      X2              =   181
      Y1              =   0
      Y2              =   32
   End
   Begin VB.Line Line1 
      BorderColor     =   &H00A86684&
      Index           =   0
      X1              =   180
      X2              =   180
      Y1              =   0
      Y2              =   31
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "Re&vert"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   4
      Left            =   4080
      TabIndex        =   7
      Top             =   120
      Width           =   480
   End
   Begin VB.Image imgRevert 
      Height          =   240
      Left            =   3720
      Top             =   120
      Width           =   240
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "&Load"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   1
      Left            =   1320
      TabIndex        =   6
      Top             =   120
      Width           =   360
   End
   Begin VB.Image imgLoad 
      Height          =   240
      Left            =   960
      Top             =   120
      Width           =   240
   End
   Begin VB.Image imgPlay 
      Height          =   240
      Left            =   4920
      Top             =   120
      Width           =   240
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "&Play"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   5
      Left            =   5280
      TabIndex        =   5
      Top             =   120
      Width           =   300
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "&Undo"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   3
      Left            =   3240
      TabIndex        =   4
      Top             =   120
      Width           =   375
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "&Save"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   2
      Left            =   2160
      TabIndex        =   3
      Top             =   120
      Width           =   375
   End
   Begin VB.Image imgSave 
      Height          =   240
      Left            =   1800
      Top             =   120
      Width           =   240
   End
   Begin VB.Label lblButton 
      AutoSize        =   -1  'True
      BackStyle       =   0  'Transparent
      Caption         =   "&New"
      BeginProperty Font 
         Name            =   "Arial"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      ForeColor       =   &H00FFFFFF&
      Height          =   210
      Index           =   0
      Left            =   480
      TabIndex        =   2
      Top             =   120
      Width           =   345
   End
   Begin VB.Image imgNew 
      Height          =   240
      Left            =   120
      Top             =   120
      Width           =   240
   End
   Begin VB.Image imgUndo 
      Height          =   240
      Left            =   2880
      Top             =   120
      Width           =   240
   End
End
Attribute VB_Name = "FrmEditor"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
'====================================================================================
'FrmEditor - The level editor's interface
'====================================================================================
'The Pac Mar Level Editor is a basic tool for creating levels for the game. The UI is
'kept to an absolute minimum to save a) a bucket load of time and b) a second bucket
'load of documentation. The UI is designed to be minimal using just clicks and drags
'so that a complete beginner can build a level without having to learn anything.
Option Explicit

'--- APIs ---------------------------------------------------------------------------
'Enable XP themeing on scrollbars/comboboxes
Private Declare Sub InitCommonControls Lib "comctl32.dll" ()
'Open files with their applications - used to play test a level with 'Pac Mar.exe'
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hWnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long

'Create an API drawing pen for doing the white outline around things
Private Declare Function CreatePen Lib "gdi32.dll" (ByVal fnPenStyle As Long, ByVal nWidth As Long, ByVal crColor As Long) As Long
Private Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

'--- Level data ---------------------------------------------------------------------
Private WithEvents Data As clsLevelData
Attribute Data.VB_VarHelpID = -1
Private FilePath As String           'The full file path
Private FileTitle As String          'The file name, sans extension
Private Scroll As PM_XY              'Scrolling offset

Private Type PM_UNDO
    Difficulty As PM_DIFFICULTYLEVEL
    Mar As PM_XY
    Pen As PM_XY
    Theme As String
    Tile() As Byte
End Type
Private Undo(9) As PM_UNDO

'--- Editor specifics ---------------------------------------------------------------
Private MyIsDirty As Boolean         'Any changes since last save?

'Which item(s) the mouse is hovering over
Private Enum PM_HOVER
    PM_HOVER_NONE = 0
    PM_HOVER_WALL_TOP = PM_WALL_TOP
    PM_HOVER_WALL_RIGHT = PM_WALL_RIGHT
    PM_HOVER_WALL_BOTTOM = PM_WALL_BOTTOM
    PM_HOVER_WALL_LEFT = PM_WALL_LEFT
    PM_HOVER_WALL_ALL = PM_WALL_ALL
    PM_HOVER_COOKIE_TOP = PM_COOKIE_TOP
    PM_HOVER_COOKIE_LEFT = PM_COOKIE_LEFT
    PM_HOVER_COOKIE_CENTER = PM_COOKIE_CENTER
    PM_HOVER_HEART = PM_HEART
    PM_HOVER_PEN = 512
    PM_HOVER_MAR = 1024
End Enum
Private HoverX As Long               'The tile X position the mouse is over
Private HoverY As Long               'The tile Y position the mouse is over
Private HoverItem As PM_HOVER        'Which element (wall / cookie) mouse is over

Private OldHoverItem As PM_HOVER
Private OldHoverLocation As PM_XY

'Variables for keeping track of items being dragged
Private DragX As Long, DragY As Long
Private DragItem As PM_HOVER

'Somewhere to store some graphics
Private ToolbarTop As New ImageDC
Private ToolbarBottom As New ImageDC
Private hPen As Long

'====================================================================================
'FORM Initialize
'====================================================================================
Private Sub Form_Initialize()
    'Apply XP theming to the scrollbar / combo boxes. There's also a Manifest file
    'that is compiled into the EXE that makes this work. See the 'Inlcudes' folder
    InitCommonControls
    
    'Create our data class to load/save and store levels
    Set Data = New clsLevelData
    
    'Set the scroll offset to display the map, to one tile X & Y. The map is
    'surrounded by a 1 tile line of walls, but these are not visisble to the user.
    'The walls are used to provide a safe buffer around the level when scrolling in
    'game so that no black areas appear outside the level. Also the wrap-around
    'tunnels punch through this one wall outline.
    Scroll.X = TileSize: Scroll.Y = TileSize
    
    'Create a white drawing pen to draw the outlines when hovering over things
    hPen = CreatePen(0, 2, vbWhite)
End Sub

'GET IsDirty: If the level has unsaved changes
'====================================================================================
Public Property Get IsDirty() As Boolean: IsDirty = MyIsDirty: End Property

'LET IsDirty
'====================================================================================
Public Property Let IsDirty(ByVal Flag As Boolean)
    'Don't need to change the property if you're changing it to the same thing
    If Flag <> MyIsDirty Then
        'Set the dirty flag to mark the level as having unsaved changes
        MyIsDirty = Flag
        'Update the title
        p_SetTitle
    End If
End Property

'LET Difficulty
'====================================================================================
Public Property Let Difficulty(ByVal Value As Long)
    'if the difficulty level has actually changed
    If Value <> Data.Difficulty Then
        Data.Difficulty = Value
        Me.IsDirty = True
    End If
End Property

'GET Tile: Return the tile information at a particular tile
'====================================================================================
Public Property Get Tile(ByVal X As Long, ByVal Y As Long, Optional ByVal ReturnRealValue As Boolean = False) As Byte
    'get value to begin
    If ReturnRealValue = True Then
        Tile = Data.Tile(X, Y)
    Else
        Tile = Data.ValidateTile(X, Y)
    End If
End Property

'====================================================================================
'FORM Load: Prepare the form
'====================================================================================
Private Sub Form_Load()
    Dim tPath As String
    Dim tName As String
    Dim K As Long
    
    p_SetTitle
    
    'Apply the 32-bit Icon to the form and get rid of that ugly 16-colour thing
    SharedIconRes.SetIcon Me.hWnd, "MAINICON", True
    
    'Get the list of themes. Each is a folder in the gfx\Themes folder
    tPath = AppPath & "\gfx\Themes\"
    'Get the first file in the Themes folder...
    tName = Dir(tPath, vbDirectory)
    Do While tName <> ""   ' Start the loop.
        'Directory lists start with "." & ".." which represent the current folder
        'and the parent folder; just ignore these two
        If tName <> "." And tName <> ".." Then
            'Verify that it's a folder
            If (GetAttr(tPath & tName) And vbDirectory) = vbDirectory Then
                'Add the Theme name to the drop down list
                Me.cmbTheme.AddItem tName
            End If
        End If
        'Get the next item in the directory..
        tName = Dir
    Loop
    
    'Load the graphics for the toolbars, draw them on
    ToolbarTop.CreateFromPicture LoadPicture(AppPath & "\gfx\HUD\StatusBarTop.bmp")
    ToolbarBottom.CreateFromPicture LoadPicture(AppPath & "\gfx\HUD\StatusBarBottom.bmp")
    
    ToolbarTop.Stretch Me.hDC, 0, 0, Me.ScaleWidth, ToolbarTop.Height
    ToolbarBottom.Stretch Me.hDC, 0, (Me.PicWorkspace.Top + Me.PicWorkspace.Height), Me.ScaleWidth, ToolbarBottom.Height
    gfx.Cookie.DrawEx Me.hDC, 595, (Me.PicWorkspace.Top + Me.PicWorkspace.Height) + 6, 16, 16, 0, 0
    
    'Load the button icons
    'NB: I've used a cheap and nasty hack for creating the toolbar, just using empty
    '    image controls to create a clickable area in front of an icon / label. This
    '    is only so there's no need to include Common Controls 6 for the toolbar
    '    control, because I would then need to include the comctl32.dll file in the
    '    installer and it's not worth the size increase.
    Me.imgNew.Picture = LoadPicture(AppPath & "\gfx\Editor\New.gif")
    Me.imgLoad.Picture = LoadPicture(AppPath & "\gfx\Editor\Load.gif")
    Me.imgSave.Picture = LoadPicture(AppPath & "\gfx\Editor\Save.gif")
    Me.imgUndo.Picture = LoadPicture(AppPath & "\gfx\Editor\Undo.gif")
    Me.imgRevert.Picture = LoadPicture(AppPath & "\gfx\Editor\Revert.gif")
    Me.imgPlay.Picture = LoadPicture(AppPath & "\gfx\Editor\Play.gif")
    Me.imgTheme.Picture = LoadPicture(AppPath & "\gfx\Editor\Theme.gif")
    Me.imgDifficulty.Picture = LoadPicture(AppPath & "\gfx\Editor\Difficulty.gif")
    
    'Apply the hand/link cursor to various parts of the UI
    For K = 0 To Me.imgButton.Count - 1
        'Toolbar buttons
        p_ApplyHandCursor Me.imgButton(K)
    Next
    'Combo boxes at the bottom
    p_ApplyHandCursor Me.cmbDifficulty
    p_ApplyHandCursor Me.cmbTheme
    'Misc.
    p_ApplyHandCursor Me.VScrLevel
    
    'Was a level opened with the editor from Explorer? (Right-click > Edit)
    If Command() <> "" Then
        'Load that level
        Me.LoadFile Replace(Command(), Chr(34), "")
    Else
        'Create a new blank level to start
        Me.NewLevel
    End If
    
    'Render the display
    Render
End Sub

'====================================================================================
'NewLevel: create a new blank level
'====================================================================================
Public Sub NewLevel(Optional ByVal Width As Long = 10, Optional ByVal Height As Long = 11, Optional ByVal TileSet As String = "Candy")
    Dim X As Long, Y As Long
    Dim Cell As Byte
    
    FileTitle = "Untitled"
    
    'Set the size of the level
    Data.Initialize Width + 2, Height + 2
    
    'Load the Wall graphics
    Data.Theme = TileSet
    
    'Clear the dirty bit to mark the level as having no unsaved changes
    Me.IsDirty = False
    p_ClearUndoSteps
    Level_Load
End Sub

'====================================================================================
'ToggleItem: Show / Hide Wall(s)
'====================================================================================
Public Sub ToggleItem(ByVal X As Long, ByVal Y As Long, ByVal Value As Byte, Optional ByVal IgnoreSafetyChecks As Boolean = False)
    Dim Vec As PM_XY
    Dim Cell As Byte
    Dim Dir As PM_DIRECTION
    Dim K As Long
    
    'This function can be very complex (especially when it starts recursively calling
    'itself). Primarily it does three things:
    ' 1) Take an all-walls tile and opening it up by punching through the walls to
    '    any tiles surrounding it that are not also entirely solid. If a tile is
    '    surrounded by three solid tiles it would create a dead-end tile which is
    '    not allowed, so the routine will call itself to continue drilling until an
    '    opening is found. If the drilling hits the side of the level, it'll turn the
    '    corner and keep drilling until an opening is found.
    '
    ' 2) Take a single wall (North, East, South, West) and remove/show it, making
    '    sure to pair up a South wall with the North wall on the tile below, etc.
    '    Also, if a wall is added, and it causes a tile to have three walls, then the
    '    dead end must be converted to a solid tile, and the tiles surrounding that
    '    checked to see if they also subsequently became dead ends - and so on...
    '
    ' 3) Toggle Cookies/Hearts on and off. Thankfully this one is simple
    
    Dim BlankCount As Long
    Dim BlankDir As PM_DIRECTION
    
    'Tunnels cannot be drilled through the top of the level, ignore tiles outside
    If (Y <= 0) Or (Y >= Data.Height) Then Exit Sub
    'The 'Toggle' paramter specifies what wall(s)/cookies to toggle on or off.
    'A Value of 15 (WALL_ALL) will remove all walls. A value of 0 though means
    'nothing to toggle, so it should be ignored.
    If Value = 0 Then Exit Sub
    
    'If you've taken out a solid tile (all walls filled in), holes need to be
    'punched out around the tile. For example, If the tile to the north has a south
    'wall, then it needs to be removed to create an opening between the two
    If (Value And PM_WALL_ALL) = PM_WALL_ALL Then '--------------------------------------
        'Check the four tiles around the tile clicked, any that are not solid tiles
        'the wall on the current tile can be punched through.
        For K = 0 To 3
            'Now here's a problem: If you create a wrap-around tunnel through the
            'side of the level, and then make another above or below, this routine
            'will punch holes through the north/south walls of each tunnel joining
            'them together offscreen. This is bad because ghosts could then navigate
            'between the two tunnels whilst offscreen, and more so, you won't be able
            'to see these hidden joins, making things very unfair. In order to
            'prevent this, we don't bother checking the north and south walls when
            'drilling a tunnel offscreen.
            If (X = (Data.Width - 1) Or X = 0) And (K = 0 Or K = 2) Then
                'Skip onto the next direction
                K = K + 1
            End If
            
            Dir = (2 ^ K)
            Vec = SharedMath.GetVectorFromDirection(Dir)
            
            'If you create a tunnel through the left or right of the level, it wraps
            'around to the other side of the level. These next two lines wrap the
            'left and the right so that this function sees no edge between the two
            '(i.e. the leftmost wall of the level backs directly onto the rightmost
            ' wall in the level).
            If (X + Vec.X) < 0 Then Vec.X = Data.Width - 1
            If (X + Vec.X) = Data.Width Then Vec.X = 0 - (Data.Width - 1)
            
            'You can't check a tile that doesn't exist
            If (Y + Vec.Y > 0) And (Y + Vec.Y < Data.Height) Then
                'Ignore solid walls. There's no opening available
                If (Data.Tile(X + Vec.X, Y + Vec.Y) And PM_WALL_ALL) <> PM_WALL_ALL Then
                    'If we've looked to the north, then remove the south wall:
                    '"GetReverseDirection(Dir)"
                    ToggleItem (X + Vec.X), (Y + Vec.Y), SharedMath.GetReverseDirection(Dir), True
                    'Count the number of openings around the original tile
                    BlankCount = (BlankCount + 1)
                    'Remember the direction this opening is from the original tile
                    '(This will be used later to determine the direction to drill)
                    BlankDir = Dir
                End If
            End If
        Next
        'Problem: If you have only one blank tile out of four, a dead end is created
        'in the tile. To fix this a tunnel has to be drilled through the wall until
        'an exit is made out the otherside
        If BlankCount = 1 Then
            'Display the progress, you'll see the drilling happen tile by tile
            Render
            'The direction to drill is the opposite of the direction the only blank
            'tile was found from the tile that was clicked - get that?
            BlankDir = SharedMath.GetReverseDirection(BlankDir)
            
            Vec = GetVectorFromDirection(BlankDir)
            'If the tunnel hits a perimiter wall a dead end will be created,
            'there's no option but to turn a corner. First, if the tunnel hits the
            'top or bottom walls then...
            If (Y + Vec.Y) = 0 Or (Y + Vec.Y) = (Data.Height - 1) Then
                'Send a tunnel left
                If X > 2 Then ToggleItem X - 1, Y, 15, True
                'And then right
                If X < (Data.Width - 2) Then ToggleItem X + 1, Y, 15, True
                
            ElseIf (X + Vec.X) = 0 Or (X + Vec.X) = Data.Width - 1 Then
                'Send a tunnel North
                If Y > 2 Then ToggleItem X, Y - 1, 15, True
                'Send a tunnel South
                If Y < (Data.Height - 2) Then ToggleItem X, Y + 1, 15, True
                
            ElseIf (X + Vec.X) = -1 Then
                'If you're drilling through the left side of the level, open a hole
                'on the right hand side to create a wrap-around tunnel
                Vec.X = (Data.Width - 1)
                ToggleItem (X + Vec.X), (Y + Vec.Y), 15, True
                
            ElseIf (X + Vec.X) > (Data.Width - 1) Then
                Vec.X = 0 - (Data.Width - 1)
                ToggleItem (X + Vec.X), (Y + Vec.Y), 15, True
                
            Else
                'Keep going until another blank tile is reached
                ToggleItem (X + Vec.X), (Y + Vec.Y), 15, True
            End If
        End If
    
    'If you've added/removed a single wall
    ElseIf (Value And PM_WALL_ALL) > 0 Then '------------------------------------------
        'Flip the bit that's being changed
        Data.ToggleItem X, Y, Value
        
        'Find the tile opposite to the one you clicked. Remember that the West
        'wall of one tile is also the East wall of the adjacent tile. Both of
        'these walls have to be set otherwise you get half a wall!
        Vec = GetVectorFromDirection(Value And PM_WALL_ALL)
        If (X + Vec.X) < 0 Then Vec.X = (Data.Width - 1)
        If (X + Vec.X) = Data.Width Then Vec.X = 0 - (Data.Width - 1)
        
        'Flip the bit for the opposite facing wall (i.e. West becomes East)
        Data.ToggleItem X + Vec.X, Y + Vec.Y, GetReverseDirection(Value And PM_WALL_ALL)
        
        'Pac Mar levels do not allow any tiles that have three walls. (A dead end)
        'The tile you clicked, and its adjacent tile have to be checked to see
        'if the wall you added didn't create any dead end tiles
        If IgnoreSafetyChecks = False Then
            p_CheckForDeadEndTile X, Y
            p_CheckForDeadEndTile (X + Vec.X), (Y + Vec.Y)
        End If
     
    'If you've toggled a cookie / heart
    ElseIf (Value And (PM_COOKIE_ALL Or PM_HEART)) > 0 Then '------------------------
        'Flip the bit that's being changed
        Data.ToggleItem X, Y, Value
            
    End If
    'Set the dirty bit to mark the level as having unsaved changes
    Me.IsDirty = True
    'Refresh the display
    Render
End Sub

'====================================================================================
'AddUndoStep: Add the current level structure to the undo buffer
'====================================================================================
Public Sub AddUndoStep()
    Dim K As Long
    'This sub is called when a change happens, copying the current level to a slot
    'in the undo buffer.

    'Move up the current buffer and get rid of the step at the end
    For K = UBound(Undo) - 1 To 0 Step -1
        p_CopyUndoStep Undo(K), Undo(K + 1)
    Next
    'Copy the current level to the top slot
    
    Dim DataUndo As PM_UNDO
    Dim X As Long, Y As Long
    With DataUndo
        .Difficulty = Data.Difficulty
        .Mar.X = Data.MarX: .Mar.Y = Data.MarY
        .Pen.X = Data.PenX: .Pen.Y = Data.PenY
        .Theme = Data.Theme
        ReDim .Tile(Data.Width - 1, Data.Height - 1)
        For Y = 0 To (Data.Height - 1): For X = 0 To (Data.Width - 1)
            .Tile(X, Y) = Data.Tile(X, Y)
        Next X: Next Y
    End With
    p_CopyUndoStep DataUndo, Undo(0)
End Sub

'====================================================================================
'CopyUndoStep
'====================================================================================
Private Sub p_CopyUndoStep(ByRef UndoFrom As PM_UNDO, ByRef UndoTo As PM_UNDO)
    Dim X As Long, Y As Long

    UndoTo.Difficulty = UndoFrom.Difficulty
    UndoTo.Mar = UndoFrom.Mar
    UndoTo.Pen = UndoFrom.Pen
    UndoTo.Theme = UndoFrom.Theme
    ReDim UndoTo.Tile(Data.Width - 1, Data.Height - 1) As Byte
    For Y = 0 To (Data.Height - 1): For X = 0 To (Data.Width - 1)
        UndoTo.Tile(X, Y) = UndoFrom.Tile(X, Y)
    Next X: Next Y
End Sub

'====================================================================================
'UndoStep
'====================================================================================
Public Sub UndoStep()
    Dim K As Long, X As Long, Y As Long
    If Undo(0).Theme <> "" Then
        'Roll back a step
        With Undo(0)
            Data.Difficulty = .Difficulty
            Data.MarX = .Mar.X: Data.MarY = .Mar.Y
            Data.PenX = .Pen.X: Data.PenY = .Pen.Y
            Data.Theme = .Theme
            For Y = 0 To (Data.Height - 1): For X = 0 To (Data.Width - 1)
                Data.Tile(X, Y) = .Tile(X, Y)
            Next X: Next Y
        End With

        For K = 0 To UBound(Undo) - 1
            p_CopyUndoStep Undo(K + 1), Undo(K)
        Next
        Level_Undo
    End If
End Sub

'====================================================================================
'PRIVATE ClearUndoSteps
'====================================================================================
Private Sub p_ClearUndoSteps()
    Dim K As Long

    For K = 0 To UBound(Undo)
        Undo(K).Difficulty = 0
        Undo(K).Mar.X = 0: Undo(K).Mar.Y = 0
        Undo(K).Pen.X = 0: Undo(K).Pen.Y = 0
        Undo(K).Theme = ""
        ReDim Undo(K).Tile(Data.Width - 1, Data.Height - 1) As Byte
    Next
    'AddUndoStep
End Sub

'====================================================================================
'Load: Read in a level file
'====================================================================================
Public Sub LoadFile(ByVal Path As String)
    Data.Load Path
    FilePath = Path
    
    'Extract the title from the full path
    Dim SplitString() As String, FileName As String
    SplitString = Split(FilePath, "\")
    FileName = SplitString(UBound(SplitString))
    FileTitle = Left(FileName, Len(FileName) - Len(Mid(FileName, InStrRev(FileName, "."))))
    
    'Clear the dirty bit, to mark the level having no unsaved changes
    Me.IsDirty = False
    p_ClearUndoSteps
    Level_Load
End Sub

'====================================================================================
'Save
'====================================================================================
Public Function Save(ByVal Path As String)
    Data.Save Path
    FilePath = Path
    
    'Extract the title from the full path
    Dim SplitString() As String, FileName As String
    SplitString = Split(FilePath, "\")
    FileName = SplitString(UBound(SplitString))
    FileTitle = Left(FileName, Len(FileName) - Len(Mid(FileName, InStrRev(FileName, "."))))

    'Clear the dirty bit, to mark the level having no unsaved changes
    Me.IsDirty = False
End Function

Public Function SaveAs(ByVal Path As String)
    '"SaveAs" is used to playtest the level by saving a temporary location
    Data.Save Path
End Function

'====================================================================================
'GetHoverItem
'====================================================================================
Public Function GetHoverItem(ByVal X As Long, ByVal Y As Long)
    Dim PixelX As Long, PixelY As Long
    Dim Cell As Long
    
    Dim Vec As PM_XY
    Dim NewX As Long, NewY As Long
    
    'Find which tile the mouse is in
    HoverX = (X + Scroll.X) \ TileSize: NewX = HoverX
    HoverY = (Y + Scroll.Y) \ TileSize: NewY = HoverY
    'Find the number of pixels away from the corner of that tile
    PixelX = (X + Scroll.X) Mod TileSize
    PixelY = (Y + Scroll.Y) Mod TileSize
    HoverItem = PM_HOVER_NONE
    
    Cell = Me.Tile(HoverX, HoverY, True)
    If (Cell And PM_WALL_TOP) = 0 And _
       (PixelX >= 27 And PixelX <= 40 And PixelY >= 0 And PixelY <= 8) _
    Then
        'Top cookie (note that it overlaps 8 pixels at the top of a tile)
        HoverItem = (HoverItem Or PM_HOVER_COOKIE_TOP)
        
    ElseIf (Cell And PM_WALL_BOTTOM) = 0 And _
           (PixelX >= 27 And PixelX <= 40 And PixelY >= 56) _
    Then
        'Bottom cookie (which is technically the top half of the top cookie on the
        'tile below)
        HoverItem = (HoverItem Or PM_HOVER_COOKIE_TOP)
        NewY = HoverY + 1
        
    ElseIf (PixelX >= 27 And PixelX <= 40 And PixelY >= 27 And PixelY <= 40) Then
        'Center cookie
        HoverItem = (HoverItem Or PM_HOVER_COOKIE_CENTER)
        
    ElseIf (Cell And PM_WALL_LEFT) = 0 And _
           (PixelX >= 0 And PixelX <= 8 And PixelY >= 27 And PixelY <= 40) _
    Then
        'Left cookie (half of it)
        HoverItem = (HoverItem Or PM_HOVER_COOKIE_LEFT)
        
    ElseIf (Cell And PM_WALL_RIGHT) = 0 And _
           (PixelX >= 56 And PixelY >= 27 And PixelY <= 40) _
    Then
        'Right cookie (which is technically the left half of the left cookie on the
        'tile to the right)
        HoverItem = (HoverItem Or PM_HOVER_COOKIE_LEFT)
        NewX = HoverX + 1
    End If
    
    'Exclude cookies from certain areas ---------------------------------------------
    'Don't allow cookies to overlap the edges of the screen
    If NewX = 1 Then HoverItem = (HoverItem And Not PM_HOVER_COOKIE_LEFT)
    If NewX = Data.Width - 1 Then HoverItem = (HoverItem And Not PM_COOKIE_ALL)
    
    'You can't change any cookies around the pen
    If (NewX >= Data.PenX And NewX <= Data.PenX + 2) And _
       (NewY >= Data.PenY - 1 And NewY <= Data.PenY + 1) _
    Then
        If NewX = (Data.PenX + 2) And NewY <> (Data.PenY + 1) Then
            HoverItem = (HoverItem And Not PM_HOVER_COOKIE_LEFT)
        
        ElseIf NewX <> (Data.PenX + 2) And NewY = (Data.PenY + 1) Then
            HoverItem = (HoverItem And Not PM_HOVER_COOKIE_TOP)
            
        ElseIf NewX <> (Data.PenX + 2) And NewY <> (Data.PenY + 1) Then
            'Hide all cookies around the pen
            HoverItem = (HoverItem And Not PM_COOKIE_ALL)
        End If
    End If
    
    'There is no cookie underneath Pac Mar's starting position
    If (NewX = Data.MarX) And (NewY = Data.MarY) Then
        HoverItem = (HoverItem And Not PM_HOVER_COOKIE_LEFT)
    End If
    
    'Hearts cover a centre cookie
    If (Tile(HoverX, HoverY) And 128) = 128 And _
       (PixelX >= 16 And PixelX <= 48) And (PixelY >= 16 And PixelY <= 48) _
    Then
        HoverItem = 128
    End If
    
    'The top 8 pixels of the tile is the north wall (ignoring the wall at the top
    'of the screen which you can't punch through)
    If (PixelY <= 8) And (HoverY > 1) And (HoverItem And PM_HOVER_COOKIE_TOP) = 0 Then
        HoverItem = (HoverItem Or 1)
    
    'The bottom 8 pixels of the tile is the south wall (ignoring the wall at the
    'bottom of the screen which you can't punch through)
    ElseIf (PixelY >= TileSize - 8) And (HoverY < Data.Height - 2) And _
           (HoverItem And PM_HOVER_COOKIE_TOP) = 0 _
    Then
        HoverItem = 4
        NewY = HoverY
        
    'The left 8 pixels of the tile is the west wall
    ElseIf (PixelX <= 8) And (HoverItem And PM_HOVER_COOKIE_LEFT) = 0 Then
        HoverItem = (HoverItem Or 8)
        
    'The right 8 pixels of the tile is the east wall
    ElseIf (PixelX >= TileSize - 8) And (HoverItem And PM_HOVER_COOKIE_LEFT) = 0 Then
        HoverItem = (HoverItem Or 2)
        NewX = HoverX
    
    End If
    
    'Over the pen?
    If (HoverX >= Data.PenX) And (HoverX <= (Data.PenX + 1)) And _
       (HoverY = Data.PenY) _
    Then
        HoverItem = PM_HOVER_PEN
    End If
    
    'You cannot place a piller where Kroc is (above the pen), or within the pen
    If NewX = Data.PenX And (NewY = Data.PenY Or NewY = Data.PenY - 1) Then
        HoverItem = (HoverItem And Not PM_HOVER_WALL_RIGHT)
    ElseIf NewX = (Data.PenX + 1) And (NewY = Data.PenY Or NewY = Data.PenY - 1) Then
        HoverItem = (HoverItem And Not PM_HOVER_WALL_LEFT)
    End If
    
    'If the tile the mouse is over is solid wall, then only that wall can be clicked
    If (Cell And PM_WALL_ALL) = PM_WALL_ALL Then
        'A filled in tile
        HoverItem = PM_HOVER_WALL_ALL
    End If
    
    If HoverItem > PM_HOVER_NONE And HoverItem < PM_HOVER_WALL_ALL Then
        Vec = SharedMath.GetVectorFromDirection(HoverItem)
        If (Me.Tile(HoverX + Vec.X, HoverY + Vec.Y, True) And PM_WALL_ALL) = PM_WALL_ALL Then
            HoverItem = PM_HOVER_WALL_ALL
            NewX = (HoverX + Vec.X)
            NewY = (HoverY + Vec.Y)
        End If
    End If
    
    'Over the Mar sprite?
    If (X + Scroll.X) >= ((Data.MarX * TileSize) - 28) And (X + Scroll.X) <= ((Data.MarX * TileSize) + (64 - 28)) And _
    (HoverY = Data.MarY) Then
        HoverItem = PM_HOVER_MAR
    End If
    
    HoverX = NewX
    HoverY = NewY
End Function

'====================================================================================
'PRIVATE CheckForDeadEndTile: Ensure a tile doesn't have 3/4 walls
'====================================================================================
Private Sub p_CheckForDeadEndTile(ByVal X As Long, ByVal Y As Long)
    Dim Vec As PM_XY
    Dim Cell As Byte
    Dim K As Long
    Dim Dir As PM_DIRECTION
    
    'You can't have dead ends in levels (i.e. tiles with three walls)
    'If any tile has three walls, fill in all four walls automatically. If by adding
    'the fourth wall, another dead end is created in an adjacent tile, then fill that
    'in too, and so on...
    
    'Get the wall data for the specified tile
    Cell = Me.Tile(X, Y, True)
    'Count the number of walls, if 3 then...
    If (Abs((Cell And PM_WALL_TOP) = PM_WALL_TOP) _
      + Abs((Cell And PM_WALL_RIGHT) = PM_WALL_RIGHT) _
      + Abs((Cell And PM_WALL_BOTTOM) = PM_WALL_BOTTOM) _
      + Abs((Cell And PM_WALL_LEFT) = PM_WALL_LEFT)) = 3 _
    Then
        'Refresh the screen, so you see the tunneling happen as it goes
        Render
        
        'Set the tile to a solid filled in wall (15)
        Cell = (Cell Or PM_WALL_ALL)
        Data.Tile(X, Y) = Cell
        
        'Look at each tile North, East, South & West of the current tile (X, Y):
        For K = 0 To 3
            'Generate a direction based on 0-3, in Up/Right/Down/Left order
            Dir = (2 ^ K)
            'Take 1=up, 2=right, 4=down, 8=left and turn it into an X & Y offset
            'between -1 and 1 where Y = -1 would be up, and X = -1 would be left
            Vec = SharedMath.GetVectorFromDirection(Dir)
            
            'If there's a hole punched out of the right hand side of the level (that
            'wraps around to the left hand side), then that tunnel has to be filled
            'in too, otherwise you will be left with an opening on the right side of
            'the level, but no subsequent opening on the left to travel through
            If (X + Vec.X > Data.Width - 1) Then Vec.X = 0 - (Data.Width - 1)
            'And do the same if filling in leftwards
            If (X + Vec.X < 0) Then Vec.X = (Data.Width - 1)
            
            'Don't look at a tile that's physically outside of the map...
            If (X + Vec.X >= 0) And (X + Vec.X < Data.Width) And _
               (Y + Vec.Y >= 0) And (Y + Vec.Y < Data.Height) _
            Then
                'Given this tile that is North, East, South or West of the original
                'tile, get its data value, and then add-in a wall that faces the
                'original tile. For example: This tile here is North of the origianl
                'tile (Vec.X = 0, Vec.Y = -1), so "GetReverseDirection(Dir)" turns
                '"North" into "South" and adds a south wall to the tile (which backs
                'against the original tile)
                Cell = Me.Tile(X + Vec.X, Y + Vec.Y, True) Or GetReverseDirection(Dir)
                Data.Tile(X + Vec.X, Y + Vec.Y) = Cell
                'Because a wall was added to this tile adjacent to the original tile,
                'it may have set three walls out of four on that tile - a dead end,
                'which is not allowed. The next line takes the location of that tile
                'and calls this function we're in now to check it has no dead ends:
                'Thus recursing through each tile until no deadends are left anywhere
                'on the level. This is how the editor fills-in an entire tunnel
                'automatically when you close up one end.
                p_CheckForDeadEndTile X + Vec.X, Y + Vec.Y
            End If
        Next
    End If
End Sub

'====================================================================================
'Render: Draw the game on the form
'====================================================================================
Public Function Render()
    LockWindowUpdate Me.PicWorkspace.hWnd
    
    Dim hDC As Long
    hDC = Me.PicWorkspace.hDC
    
    Dim TileXOffset As Long, TileYOffset As Long
    Dim Y As Long, X As Long
    Dim HoverRect As RECT
    
    Dim TileX As Long, TileY As Long
    TileX = GetTileFromPx(Scroll.X)
    TileY = GetTileFromPx(Scroll.Y)
    
    Data.DrawViewport hDC, 0, 0, Me.PicWorkspace.ScaleWidth, Me.PicWorkspace.ScaleHeight, Scroll.X, Scroll.Y
    
    'Draw the tiles -----------------------------------------------------------------
    For Y = TileY To SharedMath.Range(TileY + 7, 0, Data.Height - 1)
        For X = TileX To SharedMath.Range(TileX + 9, Data.Width - 1)
            TileXOffset = ((X - TileX) * TileSize) - (Scroll.X Mod 64)
            TileYOffset = ((Y - TileY) * TileSize) - (Scroll.Y Mod 64)
            'Cookies!
            If (Tile(X, Y) And PM_COOKIE_TOP) = 0 Then gfx.Cookie.DrawEx hDC, TileXOffset + 24, TileYOffset - 8, 16, 16, 16, 0
            If (Tile(X, Y) And PM_COOKIE_LEFT) = 0 Then gfx.Cookie.DrawEx hDC, TileXOffset - 8, TileYOffset + 24, 16, 16, 16, 0
            If (Tile(X, Y) And PM_COOKIE_CENTER) = 0 Then gfx.Cookie.DrawEx hDC, TileXOffset + 24, TileYOffset + 24, 16, 16, 0, 0 'different cookies for even/odd
            'Heart (Power Pill)
            If (Tile(X, Y) And 128) = 128 Then gfx.Heart.Draw hDC, TileXOffset + 16, TileYOffset + 16
            
            'Draw the Mar Sprite
            If (X = Data.MarX) And (Y = Data.MarY) Then
                gfx.Mar.Draw hDC, TileXOffset - 28, TileYOffset
            End If
            'Draw the Ghosts
            If (X = Data.PenX) And (Y = Data.PenY) Then
                gfx.Zim.Draw hDC, TileXOffset, TileYOffset
            ElseIf (X = Data.PenX + 1) And (Y = Data.PenY) Then
                gfx.Gir.Draw hDC, TileXOffset - HalfTileSize, TileYOffset
                gfx.Dib.Draw hDC, TileXOffset, TileYOffset
            ElseIf (X = Data.PenX + 1) And (Y = Data.PenY - 1) Then
                gfx.Kroc.Draw hDC, TileXOffset - HalfTileSize, TileYOffset
            End If
        Next
    Next
    
    'Draw the selection rectangle ---------------------------------------------------
    If (HoverItem > 0) Then
        TileXOffset = ((HoverX - TileX) * TileSize) - (Scroll.X Mod TileSize)
        TileYOffset = ((HoverY - TileY) * TileSize) - (Scroll.Y Mod TileSize)
        If (HoverItem And PM_COOKIE_ALL) > 0 Then
            'The Top cookie has a unique top & bottom position
            If (HoverItem And PM_COOKIE_TOP) > 0 Then HoverRect.Top = TileYOffset - 8: HoverRect.Bottom = TileYOffset + 8
            'The Left cookie has a unique left & right position
            If (HoverItem And PM_COOKIE_LEFT) > 0 Then HoverRect.Left = TileXOffset - 8: HoverRect.Right = TileXOffset + 8
            'The Left & Center cookies share the same top & bottom position
            If (HoverItem And (PM_COOKIE_LEFT + PM_COOKIE_CENTER)) > 0 Then HoverRect.Top = TileYOffset + 24: HoverRect.Bottom = TileYOffset + 40
            'The Top & Center Cookies share the same left & right position
            If (HoverItem And (PM_COOKIE_TOP + PM_COOKIE_CENTER)) > 0 Then HoverRect.Left = TileXOffset + 24: HoverRect.Right = TileXOffset + 40
        
        ElseIf (HoverItem And PM_WALL_ALL) = 15 Then
            HoverRect.Top = TileYOffset - 8
            HoverRect.Left = TileXOffset - 8
            HoverRect.Right = TileXOffset + TileSize + 8
            HoverRect.Bottom = TileYOffset + TileSize + 8
            
        ElseIf (HoverItem And 128) = 128 Then
            HoverRect.Top = TileYOffset + 16
            HoverRect.Left = TileXOffset + 16
            HoverRect.Right = TileXOffset + 48
            HoverRect.Bottom = TileYOffset + 48
            
        Else
            'North & West & East share the same top position
            If (HoverItem And (1 + 2 + 8)) > 0 Then HoverRect.Top = TileYOffset - 8
            'North & South & West share the same left position
            If (HoverItem And (1 + 4 + 8)) > 0 Then HoverRect.Left = TileXOffset - 8
            'South has a unique top position
            If (HoverItem And 4) > 0 Then HoverRect.Top = (TileYOffset + TileSize) - 8
            'East has a unique left position
            If (HoverItem And 2) > 0 Then HoverRect.Left = (TileXOffset + TileSize) - 8
            'North has a unique bottom position
            If (HoverItem And 1) > 0 Then HoverRect.Bottom = TileYOffset + 8
            'North, East & South share the same right position
            If (HoverItem And (1 + 2 + 4)) > 0 Then HoverRect.Right = TileXOffset + TileSize + 8
            'West has a unique right position
            If (HoverItem And 8) > 0 Then HoverRect.Right = TileXOffset + 8
            'West, South & East share the same bottom position
            If (HoverItem And (8 + 4 + 2)) > 0 Then HoverRect.Bottom = TileYOffset + TileSize + 8
        End If
        SharedAPI.SelectObject hDC, hPen
        SharedAPI.SelectObject hDC, SharedAPI.GetStockObject(SharedAPI.NULL_BRUSH)
        SharedAPI.Rectangle hDC, HoverRect.Left, HoverRect.Top, HoverRect.Right, HoverRect.Bottom
    End If
    
    LockWindowUpdate 0
    PicWorkspace.Refresh
End Function

Private Sub Data_CookieCountChange(ByVal CookieCount As Long)
    'The Level class contains an event that fires everytime the number of cookies
    'changes. We capture that event here so that we can update the UI with the count
    Static OldValue As Long
    'The previous value is remembered so that the label is only updated if it needs
    'to be, otherwise the label would flicker like crazy as you moved the mouse
    If CookieCount <> OldValue Then
        Me.lblCookies.Caption = "x " & CookieCount
        OldValue = CookieCount
    End If
End Sub

'====================================================================================
'PRIVATE ApplyHandCursor: Give a control a hyperlink like cursor
'====================================================================================
Private Sub p_ApplyHandCursor(ByRef Target As Object)
    'The hand cursor is loaded into a hidden picturebox on the form and referenced
    'by each control so that multiple copies of the graphic don't clog up memory
    If Me.picHand.Picture = 0 Then Me.picHand.Picture = LoadPicture(AppPath & "\gfx\Editor\Hand.cur")
    
    'Set the flag to say a custom icon is being used: VB does not natively support
    'the 'link' cursor. In this case we are using a cursor file loaded into a picture
    Target.MousePointer = MousePointerConstants.vbCustom
    Target.MouseIcon = Me.picHand.Picture
End Sub

'====================================================================================
'FORM MouseMove: When you mouse over the empty parts of the form
'====================================================================================
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    'Unlight all of the toolbar buttons (see imgButton_MouseMove)
    imgButton_MouseMove -1, 0, 0, 0, 0
End Sub

'====================================================================================
'imgButton_Click: When the user clicks a toolbar button
'====================================================================================
Private Sub imgButton_Click(Index As Integer)
    'The name of the button is in the 'Tag' property
    Select Case imgButton(Index).Tag
        Case "New":  p_doNew      'New level
        Case "Load": p_doLoad     'Load a level
        Case "Save": p_doSave     'Save current level
        Case "Undo":
            Me.UndoStep
            Render
        Case "Revert": p_doRevert 'Revert changes to saved version
        Case "Play": p_doPlay     'Playtest the level
    End Select
End Sub

'====================================================================================
'imgButton_MouseMove: When the user hovers over the toolbar buttons
'====================================================================================
Private Sub imgButton_MouseMove(Index As Integer, Button As Integer, Shift As Integer, X As Single, Y As Single)
    Dim K As Long
    
    'Remove the selection rectangle in the form
    HoverItem = 0
    'Loop over each toolbar button...
    For K = 0 To (imgButton.Count - 1)
        If Index = K Then
            'If this is the one, colour the text yellow
            If lblButton(K).ForeColor <> vbYellow Then lblButton(K).ForeColor = vbYellow
        Else
            'If not, colour text white
            If lblButton(K).ForeColor <> vbWhite Then lblButton(K).ForeColor = vbWhite
        End If
    Next
End Sub

'====================================================================================
'PRIVATE doNew: Create a new level
'====================================================================================
Private Function p_doNew() As Boolean
    Dim r As VbMsgBoxResult
    
    'The user has clicked the new button. Does the current level have unsaved changes
    If Me.IsDirty = True Then
        'Ask the usual Are You Sure
        r = MsgBox("Do you want to Save the current level before creating a New one?", vbYesNoCancel Or vbDefaultButton1 Or vbExclamation, "Save?")
        'Refresh the screen to ensure the msgbox is gone because the creating of a new
        'level might load the Candy theme, freezing the app up for a second - leaving
        'the imprint of the msgbox on the screen even after you've clicked it away.
        DoEvents
        If r = vbCancel Then p_doNew = False: Exit Function
        'If the user chose to save the current level before continuing
        If r = vbYes Then
            'if the user cancels the save, then also cancel the new level command
            If p_doSave = False Then p_doNew = False: Exit Function
        End If
    End If
    
    'Create a new blank, default level
    Me.NewLevel
    p_doNew = True
End Function

'====================================================================================
'PRIVATE doLoad: Open a level file
'====================================================================================
Private Function p_doLoad() As Boolean
    Dim r As VbMsgBoxResult
    Dim sFile As SelectedFile
    
    'Does the current level have unsaved changes
    If Me.IsDirty = True Then
        'If the current level is unsaved, ask
        r = MsgBox("Do you want to Save the current level?", vbYesNoCancel Or vbDefaultButton1 Or vbExclamation, "Load?")
        DoEvents
        If r = vbCancel Then p_doLoad = False: Exit Function
        If r = vbYes Then
            'If the user cancels the save, also cance the load
            If p_doSave = False Then p_doLoad = False: Exit Function
        End If
    End If
    
    'Disable the display, to avoid stray clicks. For example, if you double click
    'a file in the Open form then a click may go through to the level window
    Me.Enabled = False
    
    'Trap errors
    On Local Error GoTo LblError
    'Only allow pmlvl files to be chosen
    FileDialog.sFilter = "Pac Mar Level Files" & Chr$(0) & "*.pmblvl;*.pmlvl"
    FileDialog.flags = OFN_EXPLORER Or OFN_LONGNAMES Or OFN_HIDEREADONLY
    FileDialog.sDlgTitle = "Load Level"
    'Start in My Documents
    FileDialog.sInitDir = SharedAPI.GetSpecialFolder(CSIDL_PERSONAL)
    'Show the Save form
    sFile = ShowOpen(Me.hWnd)
    'If there was no error in doing that
    If Err.Number <> 32755 And sFile.bCanceled = False Then
        'Refresh the screen to ensure the save form goes away whilst the level loads
        DoEvents
        'Actually load the level data
        Me.LoadFile sFile.sLastDirectory & sFile.sFiles(1)
        'Return result successful
        p_doLoad = True
    Else
        'If the user cancled
        p_doLoad = False
    End If
    'Re-enable the display
    Me.Enabled = True
    Exit Function

LblError:
    'Error?
    If Err.Number > 0 Then
        MsgBox "An unknown error occured. The system returned:" & vbCrLf & vbCrLf & Err.Number & ": " & Err.Description & vbCrLf & vbCrLf & "The level was not loaded. Try again or restart the program.", vbCritical, "Critical Error"
        p_doLoad = False
    End If
    'Re-enable the display
    Me.Enabled = True
End Function

'====================================================================================
'PRIVATE doSave: Save a level
'====================================================================================
Private Function p_doSave()
    'If the level hasn't been saved before then prompt for name / location
    If FilePath = "" Then
        'If there's an error, trap it
        On Local Error GoTo LblError
        Dim sFile As SelectedFile
        
        'Prepate the save dialog:
        With FileDialog
            'Show only Pac Mar level files
            .sFilter = "Pac Mar Level File" & Chr$(0) & "*.pmblvl"
            .flags = OFN_HIDEREADONLY
            .sDlgTitle = "Save Level"
            'If no file extension is provided, apply this one
            .sDefFileExt = "pmblvl" & Chr$(0)
            .nFileExt = 6
            'Start in My Documents
            .sInitDir = SharedAPI.GetSpecialFolder(CSIDL_PERSONAL)
            'Provide the name already in the edit box
            .sFile = FileTitle
        End With
        
        'Show the save dialog
        sFile = ShowSave(Me.hWnd)
        If Err.Number <> 32755 And sFile.bCanceled = False Then
            'Actually save the file
            Me.Save sFile.sLastDirectory & sFile.sFiles(1)
            'Exit with result success
            p_doSave = True
        Else
            'Leave the file
            p_doSave = False: Exit Function
        End If
    
    'If loaded a pmlvl, must save as a pmblvl
    ElseIf LCase(Mid(FilePath, InStrRev(FilePath, "."))) = ".pmlvl" Then
        'Alert the user
        If MsgBox( _
            "This level was created with a Beta version of Pac Mar." & vbCrLf & _
            "In order to save, the level must be converted to the new format " & _
            "for Pac Mar 1.0." & vbCrLf & vbCrLf & "If you proceed, the level " & _
            "will be saved, but will no longer work with Pac Mar Beta." & vbCrLf & _
            "This is recommended, and required to edit any level made with " & _
            "Pac Mar Beta", _
            vbOKCancel Or vbDefaultButton1 Or vbQuestion Or vbApplicationModal, _
            AppTitle _
        ) = vbOK Then
            'Save in new pmblvl format
            Me.Save FilePath
            FilePath = Replace(FilePath, ".pmlvl", ".pmblvl")
        End If
        
    Else
        'Save the level using the filepath from the last save/load
        Me.Save FilePath
    End If
    'Update the title bar on the app
    p_SetTitle
    p_doSave = True
    Exit Function
    
LblError:
    MsgBox "An unknown error occured. The system returned:" & vbCrLf & vbCrLf & Err.Number & ": " & Err.Description & vbCrLf & vbCrLf & "The level was not saved. Try again or restart the program.", vbCritical, "Critical Error"
    p_doSave = False
End Function

'====================================================================================
'PRIVATE doRevert
'====================================================================================
Private Function p_doRevert() As Boolean
    Dim r As VbMsgBoxResult
    
    'Revert will ignore all unsaved changes you've made and go back to the
    'last saved version of the file; Like an 'Undo-to-last-save'
    
    'You can't revert when there are no unsaved changes anyway
    If Me.IsDirty = False Then Beep: p_doRevert = False: Exit Function
    'If the level has never been saved, you can't revert
    If FilePath = "" Then Beep: p_doRevert = False: Exit Function
    
    'Ask
    r = MsgBox("This will discard all the changes you've made since you last saved, and go back to the saved version!", vbOKCancel Or vbDefaultButton2 Or vbExclamation Or vbApplicationModal, AppTitle)
    DoEvents
    If r = vbCancel Then p_doRevert = False: Exit Function
    If r = vbOK Then
        Me.LoadFile FilePath
        p_doRevert = True
    End If
End Function

'====================================================================================
'PRIVATE doPlay
'====================================================================================
Private Function p_doPlay() As Boolean
    'Playtest the level, save a copy to a temporary file and start the game.
    'You can playtest a level without having to save it
    
    'The Windows temporary path is used for this throwaway file
    Me.SaveAs SharedAPI.GetTemporaryPath & "PMLET.pmblvl"
    
    'Launch Pac Mar with the temporary level
    ShellExecute Me.hWnd, "open", AppPath & "\Pac Mar.exe", "-nologo -map:" & Chr(34) & SharedAPI.GetTemporaryPath & "PMLET.pmlvl" & Chr(34), "", 1
End Function

'====================================================================================
'LEVEL Load: When a level is loaded, or a new one created
'====================================================================================
Private Sub Level_Load()
    Dim K As Long
    
    'Set the form title
    p_SetTitle
    
    'When a new level is created, or a level loaded - update the scrollbars
    VScrLevel.Min = TileSize
    VScrLevel.Value = TileSize
    VScrLevel.SmallChange = 8
    VScrLevel.LargeChange = TileSize
    VScrLevel.Max = Range((Data.Height - 8) * TileSize, 0, 32768)
    VScrLevel.Enabled = (VScrLevel.Max > 0)
    
    'Select the difficulty level and theme name in the UI
    Me.cmbDifficulty.ListIndex = Data.Difficulty
    'Loop over each theme and select the correct one
    For K = 0 To (Me.cmbTheme.ListCount - 1)
        If Data.Theme = Me.cmbTheme.List(K) Then Me.cmbTheme.ListIndex = K: Exit For
    Next
    
    'Render the display
    Render
End Sub

Private Sub p_SetTitle()
    If FilePath = "" Then
        Me.Caption = AppTitle
        If MyIsDirty Then Me.Caption = Me.Caption & "*"
    Else
        Me.Caption = AppTitle & " - [" & FileTitle
        If MyIsDirty Then Me.Caption = Me.Caption & "*"
        Me.Caption = Me.Caption & "]"
    End If
End Sub

'====================================================================================
'cmbDifficulty_Click: When you change the difficulty level
'====================================================================================
Private Sub cmbDifficulty_Click()
    'Get rid of the dotted focus rectangle by focusing on something else. This can
    'sometimes error, usually when the form is not visible / on form load
    On Local Error GoTo LblExit
    Me.PicWorkspace.SetFocus
    
    'When you click on the difficulty level drop down, set the value in the level
    Me.Difficulty = Me.cmbDifficulty.ListIndex
    'An unsaved change has occurred, set the dirty flag
    Me.IsDirty = True
    
LblExit:
End Sub

'====================================================================================
'cmbDifficulty_KeyPress: When you change the difficulty level
'====================================================================================
Private Sub cmbDifficulty_KeyPress(KeyAscii As Integer)
    'When you cycle through the difficulty level drop down list with the keys
    cmbDifficulty_Click
End Sub

'====================================================================================
'cmbTheme_Click: When you change the level theme
'====================================================================================
Private Sub cmbTheme_Click()
    'Get rid of the dotted focus rectangle by focusing on something else. This can
    'sometimes error, usually when the form is not visible / on form load
    On Local Error GoTo LblExit
    Me.PicWorkspace.SetFocus
    
    'Add to the undo buffer
    Me.AddUndoStep
    
    'When you choose a different level theme, load it
    Data.Theme = Me.cmbTheme.List(Me.cmbTheme.ListIndex)
    'An unsaved change has occurred, set the dirty flag
    Me.IsDirty = True
    'Render the display
    Render
    
LblExit:
End Sub

'====================================================================================
'cmbTheme_KeyPress: When you change the level theme
'====================================================================================
Private Sub cmbTheme_KeyPress(KeyAscii As Integer)
    'When you cycle through themes using the keys
    cmbTheme_Click
End Sub

'====================================================================================
'LEVEL Undo
'====================================================================================
Private Sub Level_Undo()
    Dim K As Long
    'Select the difficulty level and theme name in the UI
    Me.cmbDifficulty.ListIndex = Data.Difficulty
    'Loop over each theme and select the correct one
    For K = 0 To (Me.cmbTheme.ListCount - 1)
        If Data.Theme = Me.cmbTheme.List(K) Then Me.cmbTheme.ListIndex = K: Exit For
    Next
End Sub

'====================================================================================
'PicWorkspace_MouseDown: When you click down on the level
'====================================================================================
Private Sub PicWorkspace_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    'Examine where you clicked and determine what object was clicked on...
    Me.GetHoverItem X, Y
    'If you clicked down on a heart, or Mar / The Pen, then...
    If HoverItem = 128 Or HoverItem >= 512 Then
        'Begin dragging something, remember the cell you starting dragging from
        DragX = SharedMath.GetTileFromPx(X + Scroll.X)
        DragY = SharedMath.GetTileFromPx(Y + Scroll.Y)
        'What item you're dragging
        DragItem = HoverItem
        'Get rid of the selection rectangle
        HoverItem = 0
        'Render the display
        Render
    End If
End Sub

'====================================================================================
'PicWorkspace_MouseMove: When you move the mouse over the level
'====================================================================================
Private Sub PicWorkspace_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Dim NewX As Long, NewY As Long
    
    'Unlight the toolbar buttons (so that when you move from a toolbar button, onto
    'the level display, the toolbar button will un-highlight)
    imgButton_MouseMove -1, 0, 0, 0, 0
    'Is something being dragged? (See PicWorkspace_MouseDown above)
    If DragItem > 0 Then
        'Are you dragging a heart?
        If DragItem = 128 Then
            'Calculate the new location, within the boundaries of the level
            NewX = Range(SharedMath.GetTileFromPx(X + Scroll.X), 1, Data.Width - 2)
            NewY = Range(SharedMath.GetTileFromPx(Y + Scroll.Y), 1, Data.Height - 1)
            'You can't drag a heart over another heart, also you cannot drag the
            'heart within the Ghost Pen, nor can you drag a heart over Mar.
            'this check here prevents this
            If (Me.Tile(NewX, NewY) And PM_HEART) <> PM_HEART And _
               ((NewX >= Data.PenX And NewX <= Data.PenX + 1) And _
                (NewY >= Data.PenY - 1 And NewY <= Data.PenY)) = False And _
               ((NewX >= Data.MarX - 1 And NewX <= Data.MarX) And _
                (NewY = Data.MarY)) = False _
            Then
                'Is the mouse in a different tile than where dragging started
                If (NewX <> DragX) Or (NewY <> DragY) Then
                    'Remove the heart from the old location
                    Me.ToggleItem DragX, DragY, 128
                    'Add a heart in the new location
                    Me.ToggleItem NewX, NewY, 128
                    'Update the new position to drag from
                    DragX = NewX: DragY = NewY
                    'Render the display
                    Render
                End If
            End If
            
        'Are you dragging the Ghost Pen?
        ElseIf DragItem = 512 Then
            'Calculate the new location, within the boundaries of the level
            NewX = Range(SharedMath.GetTileFromPx(X + Scroll.X), 1, Data.Width - 3)
            NewY = Range(SharedMath.GetTileFromPx(Y + Scroll.Y), 2, Data.Height - 2)
            'You can't drag the pen under Mar, nor over a heart
            If ((NewX >= Data.MarX - 2 And NewX <= Data.MarX) And _
                (NewY >= Data.MarY And NewY <= Data.MarY + 1)) = False And _
               ((Me.Tile(NewX, NewY) Or Me.Tile(NewX + 1, NewY) Or _
                 Me.Tile(NewX, NewY - 1) Or Me.Tile(NewX + 1, NewY - 1)) And PM_HEART) = 0 _
            Then
                'Update the Pen to the new location if applicable
                If (NewX <> DragX) Or (NewY <> DragY) Then
                    Data.PenX = NewX: DragX = NewX
                    Data.PenY = NewY: DragY = NewY
                    'Render the display
                    Render
                End If
            End If
        
        'Are you dragging Pac Mar?
        ElseIf DragItem = 1024 Then
            'Calculate the new location, within the boundaries of the level
            NewX = Range(SharedMath.GetTileFromPx((X + Scroll.X) + 28), 2, Data.Width - 2)
            NewY = Range(SharedMath.GetTileFromPx(Y + Scroll.Y), 1, Data.Height - 2)
            'You can't drag Mar into the pen, nor above it (directly on top of kroc),
            'nor over a heart. This check prevents this
            If ((NewX >= Data.PenX And NewX <= Data.PenX + 2) And _
                (NewY >= Data.PenY - 1 And NewY <= Data.PenY)) = False And _
               (Me.Tile(NewX - 1, NewY) And 128) = 0 And (Me.Tile(NewX, NewY) And 128) = 0 _
            Then
                'Update the location of Mar if applicable
                'Update the Pen to the new location if applicable
                If (NewX <> DragX) Or (NewY <> DragY) Then
                    Data.MarX = NewX: DragX = NewX
                    Data.MarY = NewY: DragY = NewY
                    'Render the display
                    Render
                End If
            End If
        End If
        
    Else
        'calculate which cell/item/object the user is over
        Me.GetHoverItem X, Y
        
        'If the item is draggable, show a "Move/Drag" cursor
        If HoverItem = 128 Or HoverItem >= 512 Then
            ChangeCursor IDC_SIZEALL
            
        ElseIf (HoverItem > 0) Then
            'If hovering over a wall/cookie, show a hand cursor
            ChangeCursor IDC_LINK
            
        Else
            'Otherwise, use a normal mouse pointer
            ChangeCursor IDC_ARROW
        End If
        
        'If the item the mouse is over has changed since the previous, refresh
        If (OldHoverItem <> HoverItem) Or (OldHoverLocation.X <> HoverX) Or (OldHoverLocation.Y <> HoverY) Then
            OldHoverItem = HoverItem
            OldHoverLocation.X = HoverX
            OldHoverLocation.Y = HoverY
            Render
        End If
    End If
End Sub

'====================================================================================
'PicWorkspace_MouseUp
'====================================================================================
Private Sub PicWorkspace_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
    If DragItem > 0 Then
        'Add to the undo buffer
        Me.AddUndoStep
        
        If DragItem = PM_HOVER_PEN Then
            If Data.IfItem(Data.PenX, Data.PenY, PM_WALL_RIGHT) And (Me.Tile(Data.PenX, Data.PenY) And PM_WALL_ALL) < PM_WALL_ALL Then
                Me.ToggleItem Data.PenX, Data.PenY, PM_WALL_RIGHT
            End If
            If Data.IfItem(Data.PenX, Data.PenY - 1, PM_WALL_RIGHT) And (Me.Tile(Data.PenX, Data.PenY - 1) And PM_WALL_ALL) < PM_WALL_ALL Then
                Me.ToggleItem Data.PenX, Data.PenY - 1, PM_WALL_RIGHT
            End If
            
        ElseIf DragItem = PM_HOVER_MAR Then
            If Data.IfItem(Data.MarX, Data.MarY, PM_WALL_LEFT) And (Me.Tile(Data.MarX, Data.MarY) And PM_WALL_ALL) < PM_WALL_ALL Then
                Me.ToggleItem Data.MarX, Data.MarY, PM_WALL_LEFT
            End If
        End If
        
        'Stop dragging, and mark the level as having unsaved changes
        DragItem = 0
        Me.IsDirty = True
    Else
        'Calculate which cell the user is over
        Me.GetHoverItem X, Y
        HoverItem = Range(HoverItem)
        
        If HoverItem > 0 Then
            'Add to the undo buffer
            Me.AddUndoStep
            'Toggle the item clicked
            Me.ToggleItem HoverX, HoverY, HoverItem
        End If
    End If
End Sub

'====================================================================================
'VScrLevel_Change
'====================================================================================
Private Sub VScrLevel_Change()
    Scroll.Y = Me.VScrLevel.Value
    Render
    'Remove the annoying selection dotted line when you click on a control
    'This may error if the form is not yet shown
    On Local Error Resume Next
    PicWorkspace.SetFocus
End Sub

'====================================================================================
'VScrlLevel_Scroll
'====================================================================================
Private Sub VScrLevel_Scroll(): VScrLevel_Change: End Sub

'====================================================================================
'FORM Unload: When the editor is closed
'====================================================================================
Private Sub Form_Unload(Cancel As Integer)
    Dim Response As VbMsgBoxResult
    
    'If there's unsaved changes, then ask to save
    If Me.IsDirty = True Then
        Response = MsgBox("Do you want to Save before Quitting?", vbQuestion Or vbYesNoCancel Or vbDefaultButton1, "Save?")
        If Response = vbCancel Then Cancel = True: Exit Sub
        If Response = vbYes Then
            'Try to save, if the user cancels, then cancel the unload too
            If p_doSave = False Then Cancel = True: Exit Sub
        End If
    End If
    
    'Unload the level information
    Set Data = Nothing
    
    'Free up the graphics for this form
    Set ToolbarTop = Nothing
    Set ToolbarBottom = Nothing
    DeleteObject hPen
    
    'Clear the undo buffer
    Erase Undo
    
    'Pass onto the exit routine in MainSubs
    MainSubs.EndProgram
End Sub

'=== END ============================================================================
'This work is licenced under the Creative Commons Attribution 2.5 License
